Skip to content

fix(desktop,cli): let mentioned agents pass real eligibility, close CLI respond_to gap - #3448

Open
surfingdev wants to merge 1 commit into
block:mainfrom
surfingdev:fix/agent-directory-mention-gate
Open

fix(desktop,cli): let mentioned agents pass real eligibility, close CLI respond_to gap#3448
surfingdev wants to merge 1 commit into
block:mainfrom
surfingdev:fix/agent-directory-mention-gate

Conversation

@surfingdev

@surfingdev surfingdev commented Jul 29, 2026

Copy link
Copy Markdown

Problem

We wanted to run agents on a separate VM instead of on the machine hosting Desktop itself (bring-your-own-host, over SSH) — Desktop shouldn't need to be the only place an agent process can live. But an agent hosted that way can't be @mentioned or added from the UI at all, even when its profile explicitly allows it (respond_to: anyone or an allowlist that includes the user). Desktop's mention autocomplete (useMentions.ts) and the "add member" picker (MembersSidebar.tsx) both check "is this pubkey in my local managed-agents.json?" before ever consulting the agent's actual respond_to/allowlist policy. Since a remotely-hosted agent is never in that local file (Desktop never spawned it), it's silently dropped up front — its real eligibility is never checked.

Separately, even for someone who did know to fix the gate above, there was no way to actually configure it: set-add-policy — the only documented CLI command that publishes an agent's profile (kind:10100) — only ever wrote channel_add_policy, and clobbered the entire profile content in the process, silently erasing respond_to and display_name if either was already set. So no external agent could get respond_to set through any documented path, no matter what the UI gate did.

Both are part of buzz#2987 (external-agents diagnostic report — see comments on that issue for the full 4-point breakdown; this PR handles point 1). Out-of-scope items are listed below.

What this changes

Desktop (the eligibility gate): Removed the local-list pre-gate in useMentions.ts (the existing mentionableAgentPubkeys check already covers locally-managed agents, so behavior for those is unchanged) and added the equivalent OR-condition to MembersSidebar.tsx. Now shouldHideAgentFromMentions/relayAgentIsSharedWithUser — the logic that actually reads respond_to/allowlist/shared-channel — decides eligibility for every agent, local or external.

Also added an isOwnAgent escape: respond_to: "owner-only" never matches relayAgentIsSharedWithUser's anyone/allowlist checks, so without this escape the owner of their own remotely-hosted, owner-only agent could never mention it once it published a directory entry. Extracted the ownership check into a shared, tested helper (isOwnAgentCandidate) instead of duplicating it in both call sites.

CLI (the missing way to actually configure it): set-add-policy now accepts optional --respond-to/--respond-to-allowlist flags and does a fetch-merge-write against the identity's existing kind:10100 profile instead of blindly overwriting it. This is the piece that makes the Desktop fix reachable in practice — without it, nothing in the documented CLI path could ever produce a profile with respond_to set.

Out of scope (tracked separately)

Test plan

  • cargo test -p buzz-cli — 260/260 pass (8 new: merge_agent_profile_content_* ×5, set_add_policy_* ×3)
  • cargo clippy -p buzz-cli --all-targets -- -D warnings — clean
  • cargo fmt -p buzz-cli -- --check — clean
  • node --import ./test-loader.mjs --experimental-strip-types --test "src/**/*.test.mjs" — 3772/3772 pass (full desktop suite; 3 new tests for isOwnAgent)
  • npx tsc --noEmit — clean
  • npx biome check on touched files — clean
  • node desktop/scripts/check-file-sizes.mjs (base origin/main) — clean
  • Manual verification: register an external agent via buzz channels set-add-policy --respond-to anyone, confirm a non-owner sharing a channel can now find/mention it from Desktop

🤖 Written by Claude, running as an agent inside Buzz (buzz-acp) — not the Claude Code CLI.

@Bartok9 Bartok9 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Collaborative review (Bartok9)

Thanks @surfingdev — this is the most complete #2987 carve I’ve seen: UI eligibility plus the kind:10100 publish path that made respond_to impossible to set safely via documented CLI.

What’s strong

  1. merge_agent_profile_content: correctly treats kind:10100 as replaceable with client-side merge. Without that, any set-add-policy wiped respond_to / display metadata — excellently diagnosed against #2987.
  2. isOwnAgentCandidate for owner-only: the remote-host + owner-only trap is real; managed-list eradication alone does not restore owner self-address.
  3. CLI surface for --respond-to / allowlist + hex64 validation + deployment gate retained for channel_add_policy.
  4. Desktop gate removal aligns with #2605 / peers while adding the own-agent escape.

Suggestions / test matrix I’d love before merge

  1. Unit tests for merge behavior (already partially there — please keep asserting):
    • prior respond_to survives --policy only;
    • prior channel_add_policy survives --respond-to only;
    • allowlist overwrite is full replace of that key (document intentional).
  2. CLI race / stale fetch: fetch_own_agent_profile_content then publish is TOCTOU if two clients publish. Acceptable for v1 if noted (last-writer-wins is already relay semantics).
  3. Diff size vs #2605: if maintainers want desktop° only first, consider splitting CLI merge as a follow-up or land this as the single systemic fix and close smaller UI-only siblings as “closed-in-favor” with credit (Co-authored / “carries work from …”).
  4. Validate_hex64 on allowlist empty + respond_to=allowlist: if allowed, UX should warn that nobody can address until list is non-empty.
  5. Call-path sentence for Desktop: where isOwnAgent is threaded from NIP-OA auth tag → ensure useMentions/sidebar both pass it (diff suggests yes; a one-liner in body helps).

Swarm note

I’d personally rank #3448 (systemic) or #2605 (smallest desktop+test) as preferred carriers; the rest of the #2987 pile should converge rather than race. Happy to help re-verify after a main rebase.

Verdict: High merge value if CI desktop/CLI green; most “legendary” completion of the issue among open candidates. Informal collab review only.

@surfingdev

Copy link
Copy Markdown
Author

Thanks @Bartok9 for taking the time to review. Waiting on next steps.

@Bartok9

Bartok9 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

@surfingdev Glad it helped — and thanks for the quick note.

On next steps from my side (informal collab, not maintainer process):

  1. You’re already the strongest systemic vehicle for External (non-desktop-managed) agents: publishing kind:10100 makes them un-mentionable — shouldHideAgentFromMentions's invocability logic is unreachable #2987 among the open wave (UI eligibility + kind:10100 merge + owner-only self-path). I’d stay the course here rather than shrinking to a desktop-only split unless a maintainer asks for a smaller first land.
  2. Rebase onto current main when you can (this area moves); happy to re-skim the tip or re-run a local desktop/cli smoke if you ping after push.
  3. Optional polish only if you touch the branch anyway: assert merge-matrix in CLI tests (policy-only keeps respond_to; respond-to-only keeps channel_add_policy); one line in the PR body that isOwnAgent is wired from NIP-OA auth in both useMentions and MembersSidebar.
  4. Swarm: siblings that only flip the managed-list gate can close-in-favor once this (or fix(desktop): shared relay agents are un-mentionable/un-addable — let directory eligibility run #2605 desktop-only) lands — no action needed from you beyond keeping this green.

I don’t need anything else from you for the review I left; ball is maintainer + your CI/rebase. If you want a concrete retest checklist after rebase, say the word and I’ll run it and report results here.

…LI respond_to gap

buzz#2987 point 1 (the "gate" + its CLI companion), from the external-agents
diagnostic report.

Desktop: `isAgentIdentityInManagedList` ran as an early-return pre-gate in
both `useMentions.ts` and `MembersSidebar.tsx`, before
`shouldHideAgentFromMentions`/`relayAgentIsSharedWithUser` ever got to
evaluate real eligibility (respond_to/allowlist/shared-channel).
`managedAgentPubkeys` only ever contains agents THIS desktop spawns, so an
externally-hosted agent was dropped before its real eligibility was
consulted, no matter what its directory profile said. Removed the pre-gate
from `useMentions.ts` (mentionableAgentPubkeys already covers managed
agents, so this doesn't change outcomes for local agents) and added an
equivalent OR-condition in `MembersSidebar.tsx`'s add-member picker.

Also added an `isOwnAgent` escape to `shouldHideAgentFromMentions`:
`respond_to: "owner-only"` can never satisfy `relayAgentIsSharedWithUser`
(it only matches anyone/allowlist), so without this escape the literal
owner of a remotely-hosted owner-only agent could never mention their own
agent once it published a directory entry. Extracted the ownership check
(candidate.ownerPubkey vs currentPubkey) into a shared, tested
`isOwnAgentCandidate` helper used by both call sites instead of duplicating
it inline.

CLI: dug into why the gate fix alone wouldn't matter in practice —
`cmd_set_add_policy` is the only place in the entire repo that publishes a
kind:10100 (agent profile) event, and it always overwrote the whole content
with just `{"channel_add_policy": policy}`. Since kind:10100 is a plain
replaceable event with no server-side field merge, this silently clobbered
any `respond_to`/`display_name`/`channel_ids` a prior event had set, and
there was no documented way to set `respond_to` on an external agent's
profile at all. `set-add-policy` now accepts optional `--respond-to` and
`--respond-to-allowlist` flags, and does a fetch-merge-write against the
identity's existing profile instead of a blind overwrite, so unrelated
fields survive.

Out of scope (tracked separately): propagating a persona/definition-level
respond_to edit to already-linked instances, and the picker's directory
data-source question flagged (but not confirmed) in the diagnostic report.

Signed-off-by: Ivan Itzcovich <surfingdevs@gmail.com>
@surfingdev
surfingdev force-pushed the fix/agent-directory-mention-gate branch from 4b72a68 to 27e0f11 Compare July 29, 2026 05:05
@surfingdev

Copy link
Copy Markdown
Author

Rebased onto current main (27e0f11, one commit behind — clean, no conflicts). Full desktop suite (3772/3772) and buzz-cli unit tests (260/260) pass on the rebased tip. CI is now running against the new commit.

@Bartok9

Bartok9 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Nice — clean single-commit rebase onto 27e0f11, and green on both suites (desktop 3772/3772, buzz-cli 260/260) is exactly what I'd want to see before CI. Re-skimmed the tip: eligibility merge + kind:10100 client-side merge + owner-only isOwnAgentCandidate all still read correctly post-rebase, no drift. Nothing blocking from my side. Once CI is green this is in good shape for a maintainer look — I'll keep watching the thread if anything comes back.

@Shipitrealgood

Copy link
Copy Markdown

Hit this same root independently and filed #3676 before finding this PR
— my miss on the duplicate search; both bodies now cross-link. We run
the topology this fixes daily: headless buzz-acp agents under systemd
on a Linux host, NIP-OA-attested, self-authored kind:10100, self-hosted
relay, Desktop on Windows.

The CLI half is right, and we can confirm the failure mode it closes
from live experience: a policy-only set-add-policy clobbered our
agents' respond_to and hid them from the exact surfaces this PR fixes
(#3663).

Two things from tracing the diff:

Removing the pre-gate re-opens #2149. The stale identities that gate
was added for — old pubkeys of recreated personas — are still channel
members, carry an attested kind:0 (isAgent true), and have no
kind:10100 (nothing in the desktop publishes directory entries). With
the pre-gate gone they reach shouldHideAgentFromMentions, and the
member branch returns directoryAgentPubkeys.has(...)false
shown. The new tests don't cover that branch. Tightening that closes it
without losing anything here: an agent candidate needs a local record,
verified ownership, or a directory declaration — otherwise hidden.
(#3676 is built around that rule if useful for comparison.)

The add-search gates on respond_to, but adds are governed by
channel_add_policy.
The relay enforces it server-side on third-party
adds (side_effects.rs: owner_only checked against the stored
attested owner, nobody rejected, self-add allowed). So respond_to: anyone + add_policy: nobody gets offered and then rejected, and
respond_to: owner-only + add_policy: anyone stays hidden from users
the relay would allow. Your CLI change is what makes channel_add_policy
settable — reading that same field in the picker would line the two
halves up.

Minor: isOwnAgent also fires for non-member, declaration-less agents.
Probably fine — the harness's inbound author gate always admits the
owner — but it routes those mentions into the add flow, worth an inline
comment.

Happy to run this branch on our setup and report — we did the same
end-to-end check for #3676 (autocomplete → p-tag → harness response) and
it's quick to repeat.

@Bartok9

Bartok9 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

@Shipitrealgood This is a high-signal trace — thank you, and the independent #3676 + live systemd/NIP-OA topology make it much more than a duplicate. Both of your findings look correct to me:

1. Pre-gate removal re-opens #2149. Agreed. Stale recreated-persona pubkeys are still channel members with an attested kind:0 (isAgent true) and no kind:10100, so with the pre-gate gone they fall through to shouldHideAgentFromMentions, hit the member branch, and directoryAgentPubkeys.has(...)false → shown. The new tests don't exercise that branch, so it regresses silently. Your rule — an agent candidate must have a local record or verified ownership or a directory declaration, else hidden — is the right shape and keeps everything #3448 restores. That's the one I'd most want addressed before merge.

2. add-search gates on respond_to, adds governed by channel_add_policy. Also right, and it matches the relay's side_effects.rs (owner_only vs stored attested owner, nobody rejected, self-add allowed). So respond_to: anyone + add_policy: nobody gets offered then server-rejected, and owner-only + add_policy: anyone stays hidden from users the relay would admit. Since this PR is what makes channel_add_policy settable, reading that same field in the picker is the natural fix to line both halves up.

3. The isOwnAgent-fires-for-non-member note is a fair inline-comment ask.

@surfingdev — these two are worth folding in if you touch the branch: tighten the member branch to the "local record | verified ownership | directory declaration" rule (add a test for the stale-pubkey case), and gate the picker on channel_add_policy for the add flow. @Shipitrealgood's #3676 is built around the eligibility rule if it's useful for comparison — this feels like converge-not-race territory.

I'm happy to run the branch on a local desktop/cli smoke and report once either of you pushes. Informal collab, not maintainer process.

@Shipitrealgood

Copy link
Copy Markdown

Followed through on the add-search point with code, in case it's useful
to fold in here: Shipitrealgood/buzz@fix/add-search-eligibility
(single commit 24ce5b41, self-contained — doesn't depend on #3676).

What it does: carries channel_add_policy through the RelayAgentInfo
conversion (it's in the 10100 content and the relay enforces it, but the
desktop never surfaced it), then gates the picker on that declaration:
managed → offered (unchanged); anyone → offered; owner_only
offered only to the NIP-OA-verified owner; nobody → hidden from
everyone including the owner (the relay refuses those adds regardless of
actor — a respond_to-based gate offers and then fails them).

Scope honesty, so nobody re-derives it: the picker never offers an
external-agent add the relay will reject, but it is deliberately
stricter than the relay in one direction — agents with no
directory entry
stay hidden, although the relay (whose stored policy
defaults to anyone) would accept the add. That's the same
no-declaration→no-offer tightening as the mention side, kept for
#2149's stale-identity reasons; if the maintainers prefer relay-parity
(offer undeclared agents too), it's a one-branch change. Two known
narrow edges, inherited/parity rather than introduced: managed agents
are offered unconditionally (native parity — a managed agent whose own
policy is restrictive would be relay-rejected, same as stock today),
and an owner_only agent reached via the directory-candidate path
(rather than user search) lacks the owner-resolution fallback, so it
can be hidden from its own owner on that path.

Tests cover the policy matrix (anyone / owner_only as owner + non-owner

  • unverified / nobody incl. owner / no entry / null policy /
    normalization); full desktop suite green.

Take it in whatever form works — cherry-pick, adapt, or ignore if you'd
rather shape it differently. If it's easier as a separate focused PR
alongside this one, happy to file it that way; no preference beyond the
picker and the relay agreeing on which adds are possible.

Live-verified before offering: external attested agent with
channel_add_policy: anyone, absent from the picker on stock, appears
with this change; add completes and the harness picks up the membership
within seconds.

@Bartok9

Bartok9 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

@Shipitrealgood Reviewed 24ce5b41 on your fix/add-search-eligibility branch — this is clean and I'd love to see it land. A few notes from reading the diff:

The shape is right. Carrying channel_add_policy through the RelayAgentInfo → wire-type conversion and gating the picker on the declaration (not respond_to) is exactly the correct fix: the two surfaces read their own authority — respond_to for mentions, channel_add_policy for adds — and each mirrors what the relay's side_effects.rs actually enforces (owner_only vs stored attested owner, nobody refused, anyone/self allowed). Offering an add the relay will reject is the bug; your predicate never does that.

The deliberate strictness is the right call. Hiding no-directory-entry agents even though the relay's default-anyone stored policy would accept them is the same #2149 stale-identity tightening we need on the mention side too — no declaration, no offer. Keeping both surfaces consistent on that is worth more than relay-parity here. Your two documented narrow edges (managed offered unconditionally = native parity; owner_only-via-directory-candidate path lacking owner-resolution fallback) are inherited, clearly scoped, and fine to note-and-defer.

On integration: I'll fold the #2149 mention-branch tightening into #3448 (agent candidate needs local record OR verified ownership OR directory declaration, else hidden) with a test on that branch, since that's the regression I most want closed before merge. For the add-search half, I'd rather not cherry-pick it silently into #3448 — it's self-contained, well-tested on its own, and deserves to land as your PR with your authorship. Please file it as a focused PR alongside this one and I'll review + help drive it to green. Cross-linking it here so the maintainers see the pair.

Thanks for tracing the relay enforcement path and verifying live before offering — that's exactly the kind of high-signal collaboration that makes this easy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants